/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package threadlogging.log4j;

import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Date;

import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Transport;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;

import org.apache.log4j.Layout;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.Level;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.OptionHandler;
import org.apache.log4j.spi.TriggeringEventEvaluator;
import org.apache.log4j.xml.UnrecognizedElementHandler;
import org.w3c.dom.Element;

import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.Properties;

import threadlogging.common.CyclicBuffer;
import threadlogging.common.Util;

/**
 * 
 *   similar to log4j's SMTPAppender
 * 
 */
public class SMTPAppender extends AppenderSkeleton implements
		UnrecognizedElementHandler
{

	public static final String DEFAULT_APPENDER_NAME = "email";

	private String to;
	/**
	 * Comma separated list of cc recipients.
	 */
	private String cc;
	/**
	 * Comma separated list of bcc recipients.
	 */
	private String bcc;
	private String from;
	/**
	 * Comma separated list of replyTo addresses.
	 */
	private String replyTo;
	private String subject;
	private String smtpHost;
	private String smtpUsername;
	private String smtpPassword;
	private String smtpProtocol;
	private int smtpPort = -1;
	private boolean smtpDebug = false;
	private int bufferSize = 512;
	private boolean locationInfo = false;
	private boolean sendOnClose = false;

	protected Message msg;

	protected TriggeringEventEvaluator evaluator;

	/**
	 * The default constructor will instantiate the appender with a
	 * {@link TriggeringEventEvaluator} that will trigger on events with level
	 * ERROR or higher.
	 */
	public SMTPAppender()
	{
		this(new DefaultEvaluator());
	}

	/**
	 * Use <code>evaluator</code> passed as parameter as the
	 * {@link TriggeringEventEvaluator} for this SMTPAppender.
	 */
	public SMTPAppender(TriggeringEventEvaluator evaluator)
	{
		this.evaluator = evaluator;
		this.name = DEFAULT_APPENDER_NAME;
	}

	/**
	 * Activate the specified options, such as the smtp host, the recipient,
	 * from, etc.
	 */
	public void activateOptions()
	{
		Session session = createSession();
		msg = new MimeMessage(session);

		try
		{
			addressMessage(msg);
			if (subject != null)
			{
				try
				{
					msg.setSubject(MimeUtility.encodeText(subject, "UTF-8",
							null));
				}
				catch (UnsupportedEncodingException ex)
				{
					LogLog.error("Unable to encode SMTP subject", ex);
				}
			}
		}
		catch (MessagingException e)
		{
			LogLog.error("Could not activate SMTPAppender options.", e);
		}

		if (evaluator instanceof OptionHandler)
		{
			((OptionHandler) evaluator).activateOptions();
		}
	}

	/**
	 * Address message.
	 * 
	 * @param msg
	 *            message, may not be null.
	 * @throws MessagingException
	 *             thrown if error addressing message.
	 * 
	 */
	protected void addressMessage(final Message msg) throws MessagingException
	{
		if (from != null)
		{
			msg.setFrom(getAddress(from));
		}
		else
		{
			msg.setFrom();
		}

		// Add ReplyTo addresses if defined.
		if (replyTo != null && replyTo.length() > 0)
		{
			msg.setReplyTo(parseAddress(replyTo));
		}

		if (to != null && to.length() > 0)
		{
			msg.setRecipients(Message.RecipientType.TO, parseAddress(to));
		}

		// Add CC receipients if defined.
		if (cc != null && cc.length() > 0)
		{
			msg.setRecipients(Message.RecipientType.CC, parseAddress(cc));
		}

		// Add BCC receipients if defined.
		if (bcc != null && bcc.length() > 0)
		{
			msg.setRecipients(Message.RecipientType.BCC, parseAddress(bcc));
		}
	}

	/**
	 * Create mail session.
	 * 
	 * @return mail session, may not be null.
	 * 
	 */
	protected Session createSession()
	{
		Properties props = null;
		try
		{
			props = new Properties(System.getProperties());
		}
		catch (SecurityException ex)
		{
			props = new Properties();
		}

		String prefix = "mail.smtp";
		if (smtpProtocol != null)
		{
			props.put("mail.transport.protocol", smtpProtocol);
			prefix = "mail." + smtpProtocol;
		}
		if (smtpHost != null)
		{
			props.put(prefix + ".host", smtpHost);
		}
		if (smtpPort > 0)
		{
			props.put(prefix + ".port", String.valueOf(smtpPort));
		}

		Authenticator auth = null;
		if (smtpPassword != null && smtpUsername != null)
		{
			props.put(prefix + ".auth", "true");
			auth = new Authenticator()
			{
				protected PasswordAuthentication getPasswordAuthentication()
				{
					return new PasswordAuthentication(smtpUsername,
							smtpPassword);
				}
			};
		}
		Session session = Session.getInstance(props, auth);
		if (smtpProtocol != null)
		{
			session.setProtocolForAddress("rfc822", smtpProtocol);
		}
		if (smtpDebug)
		{
			session.setDebug(smtpDebug);
		}
		return session;
	}

	/**
	 * Perform SMTPAppender specific appending actions, mainly adding the event
	 * to a cyclic buffer and checking if the event triggers an e-mail to be
	 * sent.
	 */
	public void append(LoggingEvent event)
	{

		if (!checkEntryConditions())
		{
			return;
		}

		event.getThreadName();
		event.getNDC();
		event.getMDCCopy();
		
		if (locationInfo)
		{
			event.getLocationInformation();
		}
		
		event.getRenderedMessage();
		event.getThrowableStrRep();

		if (event instanceof EmailTriggerEvent)
		{
			sendThreadTraceEmail();
		}
		else if (ThreadTrace.isEnabled())
		{
			return;
		}
		else if (event.getLevel().isGreaterOrEqual(Level.ERROR))
		{
			sendEmail(event);
		}

	}

	
	protected void sendThreadTraceEmail()
	{
		if ((ThreadTrace.getCyclicBuffer().length() > 0) && (ThreadTrace.hasError()))
		{
			sendEmail(ThreadTrace.getCyclicBuffer().reverseIterator());
		}
		
	}

	protected void sendEmail(LoggingEvent event)
	{
		
		CyclicBuffer<LoggingEvent> cb = new CyclicBuffer<LoggingEvent>(1);
		cb.add(event);
		
		sendEmail(cb.iterator());
		
	}

	/**
	 * This method determines if there is a sense in attempting to append.
	 * 
	 * <p>
	 * It checks whether there is a set output target and also if there is a set
	 * layout. If these checks fail, then the boolean value <code>false</code>
	 * is returned.
	 */
	protected boolean checkEntryConditions()
	{
		if (this.msg == null)
		{
			errorHandler.error("Message object not configured.");
			return false;
		}

		if (this.evaluator == null)
		{
			errorHandler
					.error("No TriggeringEventEvaluator is set for appender ["
							+ name + "].");
			return false;
		}

		if (this.layout == null)
		{
			errorHandler.error("No layout set for appender named [" + name
					+ "].");
			return false;
		}
		return true;
	}

	synchronized public void close()
	{

		this.closed = true;
		
		if (sendOnClose && ThreadTrace.isEnabled())
		{
			sendThreadTraceEmail();
		}
		 
	}

	InternetAddress getAddress(String addressStr)
	{
		try
		{
			return new InternetAddress(addressStr);
		}
		catch (AddressException e)
		{
			errorHandler.error("Could not parse address [" + addressStr + "].",
					e, ErrorCode.ADDRESS_PARSE_FAILURE);
			return null;
		}
	}

	InternetAddress[] parseAddress(String addressStr)
	{
		try
		{
			return InternetAddress.parse(addressStr, true);
		}
		catch (AddressException e)
		{
			errorHandler.error("Could not parse address [" + addressStr + "].",
					e, ErrorCode.ADDRESS_PARSE_FAILURE);
			return null;
		}
	}

	/**
	 * Returns value of the <b>To</b> option.
	 */
	public String getTo()
	{
		return to;
	}

	/**
	 * The <code>SMTPAppender</code> requires a {@link org.apache.log4j.Layout
	 * layout}.
	 */
	public boolean requiresLayout()
	{
		return true;
	}

	/**
	 * Send the contents of the cyclic buffer as an e-mail message.
	 */
	protected void sendEmail(Iterator<LoggingEvent> loggingEventIterator)
	{

		// Note: this code already owns the monitor for this
		// appender. This frees us from needing to synchronize on the CyclicBuffer
		
		try
		{

			StringBuffer sbuf = new StringBuffer();
			String t = layout.getHeader();
			if (t != null)
			{
				sbuf.append(t);
			}
			
			while (loggingEventIterator.hasNext())
			{

				LoggingEvent event = loggingEventIterator.next();
				
				sbuf.append(layout.format(event));
				if (layout.ignoresThrowable())
				{
					String[] s = event.getThrowableStrRep();
					if (s != null)
					{
						for (int j = 0; j < s.length; j++)
						{
							sbuf.append(s[j]);
							sbuf.append(Layout.LINE_SEP);
						}
					}
				}
			}
			
			t = layout.getFooter();
			if (t != null)
			{
				sbuf.append(t);
			}

			if ( (ThreadTrace.isEnabled()) && (ThreadTrace.getHttpServletRequest() != null) )
			{
				sbuf.append("\n");
				sbuf.append(Util.toString(ThreadTrace.getHttpServletRequest()));
			}
			
			String s = sbuf.toString();
			
			boolean allAscii = true;
			
			for (int i = 0; i < s.length() && allAscii; i++)
			{
				allAscii = s.charAt(i) <= 0x7F;
			}
			MimeBodyPart part;
			if (allAscii)
			{
				part = new MimeBodyPart();
				part.setContent(s, layout.getContentType());
			}
			else
			{
				try
				{
					ByteArrayOutputStream os = new ByteArrayOutputStream();
					Writer writer = new OutputStreamWriter(MimeUtility.encode(
							os, "quoted-printable"), "UTF-8");
					writer.write(s);
					writer.close();
					InternetHeaders headers = new InternetHeaders();
					headers.setHeader("Content-Type", layout.getContentType()
							+ "; charset=UTF-8");
					headers.setHeader("Content-Transfer-Encoding",
							"quoted-printable");
					part = new MimeBodyPart(headers, os.toByteArray());
				}
				catch (Exception ex)
				{
					for (int i = 0; i < sbuf.length(); i++)
					{
						if (sbuf.charAt(i) >= 0x80)
						{
							sbuf.setCharAt(i, '?');
						}
					}
					part = new MimeBodyPart();
					part.setContent(sbuf.toString(), layout.getContentType());
				}
			}

			Multipart mp = new MimeMultipart();
			mp.addBodyPart(part);
			msg.setContent(mp);

			msg.setSentDate(new Date());
			Transport.send(msg);
		}
		catch (MessagingException e)
		{
			LogLog.error("Error occured while sending e-mail notification.", e);
		}
		catch (RuntimeException e)
		{
			LogLog.error("Error occured while sending e-mail notification.", e);
		}
	}

	/**
	 * Returns value of the <b>EvaluatorClass</b> option.
	 */
	public String getEvaluatorClass()
	{
		return evaluator == null ? null : evaluator.getClass().getName();
	}

	/**
	 * Returns value of the <b>From</b> option.
	 */
	public String getFrom()
	{
		return from;
	}

	/**
	 * Get the reply addresses.
	 * 
	 * @return reply addresses as comma separated string, may be null.
	 * 
	 */
	public String getReplyTo()
	{
		return replyTo;
	}

	/**
	 * Returns value of the <b>Subject</b> option.
	 */
	public String getSubject()
	{
		return subject;
	}

	/**
	 * The <b>From</b> option takes a string value which should be a e-mail
	 * address of the sender.
	 */
	public void setFrom(String from)
	{
		this.from = from;
	}

	/**
	 * Set the e-mail addresses to which replies should be directed.
	 * 
	 * @param addresses
	 *            reply addresses as comma separated string, may be null.
	 * 
	 */
	public void setReplyTo(final String addresses)
	{
		this.replyTo = addresses;
	}

	/**
	 * The <b>Subject</b> option takes a string value which should be a the
	 * subject of the e-mail message.
	 */
	public void setSubject(String subject)
	{
		this.subject = subject;
	}

	/**
	 * The <b>BufferSize</b> option takes a positive integer representing the
	 * maximum number of logging events to collect in a cyclic buffer. When the
	 * <code>BufferSize</code> is reached, oldest events are deleted as new
	 * events are added to the buffer. By default the size of the cyclic buffer
	 * is 512 events.
	 */
	public void setBufferSize(int bufferSize)
	{
		this.bufferSize = bufferSize;
		// todo : getCyclicBuffer().resize(bufferSize);
	}

	/**
	 * The <b>SMTPHost</b> option takes a string value which should be a the
	 * host name of the SMTP server that will send the e-mail message.
	 */
	public void setSMTPHost(String smtpHost)
	{
		this.smtpHost = smtpHost;
	}

	/**
	 * Returns value of the <b>SMTPHost</b> option.
	 */
	public String getSMTPHost()
	{
		return smtpHost;
	}

	/**
	 * The <b>To</b> option takes a string value which should be a comma
	 * separated list of e-mail address of the recipients.
	 */
	public void setTo(String to)
	{
		this.to = to;
	}

	/**
	 * Returns value of the <b>BufferSize</b> option.
	 */
	public int getBufferSize()
	{
		return bufferSize;
	}

	/**
	 * The <b>EvaluatorClass</b> option takes a string value representing the
	 * name of the class implementing the {@link TriggeringEventEvaluator}
	 * interface. A corresponding object will be instantiated and assigned as
	 * the triggering event evaluator for the SMTPAppender.
	 */
	public void setEvaluatorClass(String value)
	{
		evaluator = (TriggeringEventEvaluator) OptionConverter
				.instantiateByClassName(value, TriggeringEventEvaluator.class,
						evaluator);
	}

	/**
	 * The <b>LocationInfo</b> option takes a boolean value. By default, it is
	 * set to false which means there will be no effort to extract the location
	 * information related to the event. As a result, the layout that formats
	 * the events as they are sent out in an e-mail is likely to place the wrong
	 * location information (if present in the format).
	 * 
	 * <p>
	 * Location information extraction is comparatively very slow and should be
	 * avoided unless performance is not a concern.
	 */
	public void setLocationInfo(boolean locationInfo)
	{
		this.locationInfo = locationInfo;
	}

	/**
	 * Returns value of the <b>LocationInfo</b> option.
	 */
	public boolean getLocationInfo()
	{
		return locationInfo;
	}

	/**
	 * Set the cc recipient addresses.
	 * 
	 * @param addresses
	 *            recipient addresses as comma separated string, may be null.
	 * 
	 */
	public void setCc(final String addresses)
	{
		this.cc = addresses;
	}

	/**
	 * Get the cc recipient addresses.
	 * 
	 * @return recipient addresses as comma separated string, may be null.
	 * 
	 */
	public String getCc()
	{
		return cc;
	}

	/**
	 * Set the bcc recipient addresses.
	 * 
	 * @param addresses
	 *            recipient addresses as comma separated string, may be null.
	 * 
	 */
	public void setBcc(final String addresses)
	{
		this.bcc = addresses;
	}

	/**
	 * Get the bcc recipient addresses.
	 * 
	 * @return recipient addresses as comma separated string, may be null.
	 * 
	 */
	public String getBcc()
	{
		return bcc;
	}

	/**
	 * The <b>SmtpPassword</b> option takes a string value which should be the
	 * password required to authenticate against the mail server.
	 * 
	 * @param password
	 *            password, may be null.
	 * 
	 */
	public void setSMTPPassword(final String password)
	{
		this.smtpPassword = password;
	}

	/**
	 * The <b>SmtpUsername</b> option takes a string value which should be the
	 * username required to authenticate against the mail server.
	 * 
	 * @param username
	 *            user name, may be null.
	 * 
	 */
	public void setSMTPUsername(final String username)
	{
		this.smtpUsername = username;
	}

	/**
	 * Setting the <b>SmtpDebug</b> option to true will cause the mail session
	 * to log its server interaction to stdout. This can be useful when debuging
	 * the appender but should not be used during production because username
	 * and password information is included in the output.
	 * 
	 * @param debug
	 *            debug flag.
	 * 
	 */
	public void setSMTPDebug(final boolean debug)
	{
		this.smtpDebug = debug;
	}

	/**
	 * Get SMTP password.
	 * 
	 * @return SMTP password, may be null.
	 * 
	 */
	public String getSMTPPassword()
	{
		return smtpPassword;
	}

	/**
	 * Get SMTP user name.
	 * 
	 * @return SMTP user name, may be null.
	 * 
	 */
	public String getSMTPUsername()
	{
		return smtpUsername;
	}

	/**
	 * Get SMTP debug.
	 * 
	 * @return SMTP debug flag.
	 * 
	 */
	public boolean getSMTPDebug()
	{
		return smtpDebug;
	}

	/**
	 * Sets triggering evaluator.
	 * 
	 * @param trigger
	 *            triggering event evaluator.
	 * 
	 */
	public final void setEvaluator(final TriggeringEventEvaluator trigger)
	{
		if (trigger == null)
		{
			throw new NullPointerException("trigger");
		}
		this.evaluator = trigger;
	}

	/**
	 * Get triggering evaluator.
	 * 
	 * @return triggering event evaluator.
	 * 
	 */
	public final TriggeringEventEvaluator getEvaluator()
	{
		return evaluator;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * 
	 */
	public boolean parseUnrecognizedElement(final Element element,
			final Properties props) throws Exception
	{
		if ("triggeringPolicy".equals(element.getNodeName()))
		{
			Object triggerPolicy = org.apache.log4j.xml.DOMConfigurator
					.parseElement(element, props,
							TriggeringEventEvaluator.class);
			if (triggerPolicy instanceof TriggeringEventEvaluator)
			{
				setEvaluator((TriggeringEventEvaluator) triggerPolicy);
			}
			return true;
		}

		return false;
	}

	/**
	 * Get transport protocol. Typically null or "smtps".
	 * 
	 * @return transport protocol, may be null.
	 * 
	 */
	public final String getSMTPProtocol()
	{
		return smtpProtocol;
	}

	/**
	 * Set transport protocol. Typically null or "smtps".
	 * 
	 * @param val
	 *            transport protocol, may be null.
	 * 
	 */
	public final void setSMTPProtocol(final String val)
	{
		smtpProtocol = val;
	}

	/**
	 * Get port.
	 * 
	 * @return port, negative values indicate use of default ports for protocol.
	 * 
	 */
	public final int getSMTPPort()
	{
		return smtpPort;
	}

	/**
	 * Set port.
	 * 
	 * @param val
	 *            port, negative values indicate use of default ports for
	 *            protocol.
	 * 
	 */
	public final void setSMTPPort(final int val)
	{
		smtpPort = val;
	}

	/**
	 * Get sendOnClose.
	 * 
	 * @return if true all buffered logging events will be sent when the
	 *         appender is closed.
	 * 
	 */
	public final boolean getSendOnClose()
	{
		return sendOnClose;
	}

	/**
	 * Set sendOnClose.
	 * 
	 * @param val
	 *            if true all buffered logging events will be sent when appender
	 *            is closed.
	 * 
	 */
	public final void setSendOnClose(final boolean val)
	{
		sendOnClose = val;
	}

	static class DefaultEvaluator implements
			org.apache.log4j.spi.TriggeringEventEvaluator
	{
		public boolean isTriggeringEvent(LoggingEvent event)
		{
			if (event instanceof EmailTriggerEvent)
			{
				return true;
			}
			else
			{
				return ((!ThreadTrace.isEnabled()))
						&& event.getLevel().isGreaterOrEqual(Level.ERROR);
			}
		}
	}

	public static void sendEmail()
	{
		if (!ThreadTrace.hasError())
		{
			return;
		}

		SMTPAppender appender = (SMTPAppender) LogManager.getRootLogger()
				.getAppender(DEFAULT_APPENDER_NAME);

		if (appender != null)
		{
			appender.append(EmailTriggerEvent.INSTANCE);
		}
	}


	public static class EmailTriggerEvent extends LoggingEvent
	{
		static public final EmailTriggerEvent INSTANCE = new EmailTriggerEvent();

		private EmailTriggerEvent()
		{
			super("", Logger.getLogger(EmailTriggerEvent.class.getName()),
					Level.ERROR, "email trigger event", null);
		}

	}

}
